package models;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToMany;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import javax.persistence.TableGenerator;

import models.TokenAction.Type;

import com.avaje.ebean.Ebean;
import com.avaje.ebean.ExpressionList;
import com.avaje.ebean.validation.Email;
import com.feth.play.module.pa.providers.password.UsernamePasswordAuthUser;
import com.feth.play.module.pa.user.AuthUser;
import com.feth.play.module.pa.user.AuthUserIdentity;
import com.feth.play.module.pa.user.EmailIdentity;
import com.feth.play.module.pa.user.FirstLastNameIdentity;
import com.feth.play.module.pa.user.NameIdentity;

import be.objectify.deadbolt.core.models.Subject;

import play.db.ebean.Model;

@Entity
@Table(name = "users")
public class User extends Model implements Subject {

	private static final long serialVersionUID = 1L;

	@Column(name = "id")
	@GeneratedValue(generator = "SEQ_TABLE", strategy = GenerationType.TABLE)
	@Id
	@TableGenerator(name = "SEQ_TABLE")
	private long id;

	@Column(name = "email")
	@Email
	private String email;

	@Column(name = "name")
	private String name;

	@Column(name = "first_name")
	private String firstName;

	@Column(name = "last_name")
	private String lastName;

	@Column(name = "company")
	@ManyToOne(fetch = FetchType.LAZY)
	private Company company;

	@Column(name = "last_login")
	private Date lastLogin;

	@Column(name = "active")
	private boolean active;

	@Column(name = "email_validated")
	private boolean emailValidated;

	@Column(name = "accounts")
	@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
	private List<Account> accounts;

	@Column(name = "roles")
	@ManyToMany
	private List<Role> roles;

	@Column(name = "permissions")
	@ManyToMany
	private List<Permission> permissions;

	public static Finder<Long, User> find = new Finder<Long, User>(long.class, User.class);

	@Override
	public String getIdentifier() {
		return Long.toString(id);
	}

	public String getEmail() {
		return email;
	}

	public void setEmail(String email) {
		this.email = email;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public String getFirstName() {
		return firstName;
	}

	public void setFirstName(String firstName) {
		this.firstName = firstName;
	}

	public String getLastName() {
		return lastName;
	}

	public void setLastName(String lastName) {
		this.lastName = lastName;
	}

	public Company getCompany() {
		return company;
	}

	public void setCompany(Company company) {
		this.company = company;
	}

	public boolean getActive() {
		return active;
	}

	public void setActive(boolean active) {
		this.active = active;
	}

	public boolean getEmailValidated() {
		return emailValidated;
	}

	public void setEmailValidated(boolean emailValidated) {
		this.emailValidated = emailValidated;
	}

	public List<Account> getAccounts() {
		return accounts;
	}

	@Override
	public List<Role> getRoles() {
		return roles;
	}

	public void addRole(Role role) {
		if (role != null && !roles.contains(role)) {
			List<Role> r = new ArrayList<Role>(roles);
			if (!r.contains(role)) {
				r.addAll(Collections.singletonList(role));
			}
			roles = r;
		}
	}

	@Override
	public List<Permission> getPermissions() {
		return permissions;
	}

	public void merge(final User otherUser) {
		for (final Account account : otherUser.accounts) {
			this.accounts.add(Account.create(account));
		}
		otherUser.active = false;
		Ebean.save(Arrays.asList(new User[] { this, otherUser }));
	}

	public Set<String> getProviders() {
		final Set<String> providerKeys = new HashSet<String>(accounts.size());
		for (final Account account : accounts) {
			providerKeys.add(account.getProviderKey());
		}
		return providerKeys;
	}

	public Account getAccountByProvider(final String providerKey) {
		return Account.findByProviderKey(this, providerKey);
	}

	public void changePassword(final UsernamePasswordAuthUser authUser, final boolean create) {
		Account account = getAccountByProvider(authUser.getProvider());
		if (account == null) {
			if (create) {
				account = Account.create(authUser);
				account.setUser(this);
			} else {
				throw new RuntimeException("Account not enabled for password usage");
			}
		}
		account.setProviderUserId(authUser.getId());
		account.save();
	}

	public void resetPassword(final UsernamePasswordAuthUser authUser, final boolean create) {
		changePassword(authUser, create);
		TokenAction.deleteByUser(this, Type.PASSWORD_RESET);
	}

	public static User create(final AuthUser authUser) {
		final User user = new User();
		user.roles = Collections.singletonList(Role.findByRoleName(controllers.Application.ROLE_USER));
		user.active = true;
		user.lastLogin = new Date();
		user.accounts = Collections.singletonList(Account.create(authUser));

		if (authUser instanceof EmailIdentity) {
			final EmailIdentity identity = (EmailIdentity) authUser;
			user.email = identity.getEmail();
			user.emailValidated = false;
		}

		if (authUser instanceof NameIdentity) {
			final NameIdentity identity = (NameIdentity) authUser;
			final String name = identity.getName();
			if (name != null) {
				user.name = name;
			}
		}

		if (authUser instanceof FirstLastNameIdentity) {
			final FirstLastNameIdentity identity = (FirstLastNameIdentity) authUser;
			final String firstName = identity.getFirstName();
			final String lastName = identity.getLastName();
			if (firstName != null) {
				user.firstName = firstName;
			}
			if (lastName != null) {
				user.lastName = lastName;
			}
		}

		user.save();
		user.saveManyToManyAssociations("roles");

		return user;
	}

	public static void merge(final AuthUser oldUser, final AuthUser newUser) {
		User.findByAuthUserIdentity(oldUser).merge(User.findByAuthUserIdentity(newUser));
	}

	public static boolean existsByAuthUserIdentity(final AuthUserIdentity identity) {
		final ExpressionList<User> exp;
		if (identity instanceof UsernamePasswordAuthUser) {
			exp = getUsernamePasswordAuthUserFind((UsernamePasswordAuthUser) identity);
		} else {
			exp = getAuthUserFind(identity);
		}
		return exp.findRowCount() > 0;
	}

	private static ExpressionList<User> getUsernamePasswordAuthUserFind(final UsernamePasswordAuthUser authUser) {
		ExpressionList<User> user = getEmailUserFind(authUser.getEmail());
		user = user.eq("accounts.providerKey", authUser.getProvider());
		return user;
	}

	private static ExpressionList<User> getEmailUserFind(final String email) {
		return find.where().eq("active", true).eq("email", email);
	}

	private static ExpressionList<User> getAuthUserFind(final AuthUserIdentity authUser) {
		return find.where().eq("active", true).eq("accounts.providerKey", authUser.getProvider()).eq("accounts.providerUserId", authUser.getId());
	}

	public static User findByAuthUserIdentity(final AuthUserIdentity identity) {
		if (identity == null) {
			return null;
		}
		if (identity instanceof UsernamePasswordAuthUser) {
			return findByUsernamePasswordIdentity((UsernamePasswordAuthUser) identity);
		} else {
			return getAuthUserFind(identity).findUnique();
		}
	}

	public static User findByUsernamePasswordIdentity(final UsernamePasswordAuthUser authUser) {
		return getUsernamePasswordAuthUserFind(authUser).findUnique();
	}

	public static void addAccount(final AuthUser oldUser, final AuthUser newUser) {
		final User user = User.findByAuthUserIdentity(oldUser);
		user.accounts.add(Account.create(newUser));
		user.save();
	}

	public static void setLastLoginDate(final AuthUser authUser) {
		final User user = User.findByAuthUserIdentity(authUser);
		user.lastLogin = new Date();
		user.save();
	}

	public static User findByEmail(final String email) {
		return getEmailUserFind(email).findUnique();
	}

	public static void verify(final User user) {
		user.emailValidated = true;
		user.save();
		TokenAction.deleteByUser(user, Type.EMAIL_VERIFICATION);
	}

}
